init('Remoting');
/**
 * Remoting.js
 * 
 * Classes:
 *  Remoting 
 *    - Creates remote proxies
 *  
 *  HttpEndpoint 
 *    - Transport for remote proxies
 *  
 *  Result
 *    - Container for remote proxy
 *  
 *  Request
 *    - Internal class to track request
 * 
 */
(function(){
	var http       = require('http'),
	  StringUtils  = require('./StringUtils.js'),
	  ObjectUtils  = require('./ObjectUtils.js'),
	  Collections  = require('./Collections.js'),
	  CommandChain = require('./CommandChain.js'),
	  log          = require('./Log.js').create('Remoting'),
	  console      = require('console');
	
	var iterate = Collections.iterate;
	
	var format  = StringUtils.format;
	
	var copy    = ObjectUtils.copy;
	
	var FUNCTION_MARKER = '__function';
	
	var RemoteEndpointFactory = function(remoting, scope){
		this.remoting = remoting;
		this.scope = scope;
	}
	RemoteEndpointFactory.prototype = {
		create : function(descriptor, callback){
			log.debug('createRemoteEndpoint ${desc}', {desc:descriptor});
			if( descriptor.__methodName ){
				this._createRemoteMethod(descriptor, callback);
			}else{
				this._createRemoteObject(descriptor, callback);
			}
		},
		canCreate : function(value){
			return value && value.__serviceName;
		},
		_createRemoteMethod : function(descriptor, callback){
			var endpoint = new HttpEndpoint(this.remoting, descriptor.__serviceName, descriptor.__methodName);
			var delegate = function(){
				log.debug('INVOKE - ${desc.__serviceName}.${desc.__methodName}', {desc:descriptor});
				var _result = endpoint.invoke.apply(endpoint, arguments);
				log.trace('${desc.__serviceName}.${desc.__methodName} = ResultId: ' +_result.__state.id);
				return _result;
			};
			log.debug('createRemoteEndpoint - end - ${desc}', {desc:descriptor});
			callback.apply(this.scope, [delegate]);
		},
		_createRemoteObject : function(descriptor, callback){
			var remoting = this.remoting;
			(function(){
				log.debug('CREATE - ${desc.__serviceName}', {desc:descriptor});
				var svc = remoting.createRemoteService(descriptor.__serviceName);
				iterate(descriptor, function(value, member){
					svc[member] = descriptor[member];
				});
				svc.on('ready', function(){
					log.debug('createRemoteEndpoint - end - ${desc}', {desc:descriptor});
					callback.apply(this.scope, [svc]);
				});
				svc.on('error', function(){
					callback.apply(this.scope, [svc]);
				});
			})();
		}
	};

	var total = 1;
	var Result = function(){
		var scope = this;
		this.__state = {
			status  : Remoting.Constants.WAITING_FOR_RESPONSE
			,id     : total++
			,parent : scope
			,notifyIf : function(event){
				log.debug('notifyIf : ${0}', event);
				if( event === 'ready' && this.status === Remoting.Constants.READY && this[event] ){
					this[event](this.parent);
				}
				if( event === 'error' && this.status === Remoting.Constants.FAILED && this[event] ){
					this[event](this.parent);
				}
			}
		}
	};
	Result.prototype = {
		on : function(event, callback){
			this.__state[event] = callback;
			this.__state.notifyIf(event);
		}
	};
	var Request = function(parent, parameters){
		this.parameters = parameters;
		this.result = new Result();
		this.httpEndpoint = parent;
	};
	Request.prototype = {
		marshall : function(){
			var args = [];
			//TODO Deep argument marshalling
			iterate(this.parameters, function(value, index){
				//If any of the arguments are functions we want a special marker so we can setup a callback
				if( typeof value === 'function' )
					args.push( FUNCTION_MARKER );
				else
					args.push(value);
			});
			var json = JSON.stringify(args);
			log.debug('Request.marshall - ${0}', json);
			return json;
		},
		onCopyComplete : function(){
			log.debug('copy : callback');
			log.trace('Result Proccess ResultId:' + this.result.__state.id);
			var r1 = this.result;
			r1.__state.status = Remoting.Constants.READY;
			r1.__state.notifyIf('ready');
		},
		copy : function(src, dest, onDone, scope){
			scope = scope || this;
			copy(src)
			  .excluding(['on', '__state'])
			  .withFactory(new RemoteEndpointFactory(this.httpEndpoint.remoting, this))
			  .withScope(scope)
			  .withCallback(onDone)
			  .into(dest);
		},
		onCtorIfComplete : function(object){
			log.debug('ctorIf : callback');
			log.trace("ResultId:"+this.result.__state.id);
			
			this.copy(object, this.result, this.onCopyComplete);
		},
		processResult : function(remoteResponse){
			log.debug('processResult : ${id} ${res}', {id:this.result.__state.id, res:remoteResponse});
			log.trace("processResult Id:"+this.result.__state.id);
			var result = {};
			var onDone = function(){
				this.onCtorIfComplete(result.object);
			};
			this.copy({object:remoteResponse.result}, result, onDone)
		},
		processCallback : function(remoteResponse){
			log.debug('processCallback');
			var callback = remoteResponse.callback;
			var args = [];
			var onDone = function(){
				var index = callback.index;
				this.invokeParameter(index, args);
			};
			this.copy(callback.parameters, args, onDone)
		},
		invokeParameter : function(index, args){
			log.debug('invokeParameter[${index}] : ${args}', {index:index, args:args});
			var callback = this.parameters[index];
			if( callback )
				callback.apply(this, args);
			else
				log("No Function at ${index}", {index:index});
		},
		onResponse : function(response){
			this.unmarshall(response, function(remoteResponse){
				//function callback
				if( remoteResponse.callback ){
					this.processCallback(remoteResponse);
				}else{
					this.processResult(remoteResponse);
				}
			});
		},
		unmarshall : function(response, callback){
			log.debug('unmarshall ${0}', response);
			var remoteResponse = JSON.parse(response);
			callback.apply(this, [remoteResponse]);
		}
	};
	
	var HttpEndpoint = function(parent, serviceName, methodName, httpMethod){
		this.remoting = parent;
		this.serviceName = serviceName;
		this.methodName  = methodName;
		this.options = {
    		host  : parent.host
    		,port : parent.port
    		,path : format('/${remoting.requestPath}/${serviceName}/${methodName}', this)
    		,method : httpMethod || 'POST'
    	};
	};
	HttpEndpoint.prototype = {
		invoke : function(){
			return this._invokeHttp(new Request(this, arguments));
		},
		_onHttpResponse : function(response, remoteRequest){
			log.debug('HttpEndpoint._onHttpResponse');
			var httpEndpoint = this;
			var body = '';
			response.on('data', function(data){
				body += data.toString();
			});
			response.on('end', function(){
				remoteRequest.onResponse.apply(remoteRequest, [body, remoteRequest]);
			});
		},
		_invokeHttp : function(remoteRequest){
			var httpEndpoint = this;
			var req = http.request(this.options, function(res){
				httpEndpoint._onHttpResponse(res, remoteRequest);
			});
			req.on('error', function(){
				remoteRequest.result.__state.status = Remoting.Constants.FAILED;
				remoteRequest.result.__state.notifyIf('error');
			});
    		var data = remoteRequest.marshall();
    		req.setHeader('content-type', 'application/json');
    		req.setHeader('content-length', data.length);
    		req.end(data);
    		return remoteRequest.result;
		}
	};
	var Remoting = function(){
		this.host = 'localhost';
		this.port = 8080;
		this.requestPath = 'invoker';
	};
	Remoting.prototype = {
		_describe : function(name){
			var service = services[name];
			if( !service ){
				return {error:_('Service \'${name}\' does not exist.', {name:name})};
			}
			var endpoints = {};
			for(member in service){
				if( typeof service[member] === 'function' && !member.match(/^_.*/) )
					endpoints[member] = member;
			}
			return endpoints;
		},
		init : function(){
			if( arguments.length == 1 ){
				this.host = arguments[0].host;
				this.port = arguments[0].port;
				this.requestPath = arguments[0].requestPath;
			}else{
				this.host = arguments[0] || this.host;
				this.port = arguments[1] || this.port;
				this.requestPath = arguments[2] || this.requestPath;
			}
		},
		create : function(host, port, requestPath){
			var clone = new Remoting();
			clone.init(host, port, requestPath);
			return clone;
		},
		createRemoteService : function(name, onComplete, onError){
			log.info('createRemoteService : ${name}', {name:name});
        	var endpoint = new HttpEndpoint(this, name, '_describe', 'GET');
			var result = endpoint.invoke.apply(endpoint, []);
			result.__service = {
				name     : name
			};
			if( onComplete ){
				result.on('ready', onComplete);
			}
			if( onError ){
				result.on('error', onError);
			}
			return result;
        }
	};
	Remoting.Constants = {
		WAITING_FOR_RESPONSE : 0,
		READY : 1,
		FAILED : -1
	};
	exports.create = function(){
		return new Remoting();
	}
	exports.ctor = Remoting;
})()